package com.dbms.execution.execution;

import com.dbms.catalog.Catalog;
import com.dbms.common.RID;
import com.dbms.common.Schema;
import com.dbms.common.Tuple;
import com.dbms.execution.plan.PlanNode;
import com.dbms.storage.Page;
import com.dbms.storage.TablePage;
import com.dbms.transaction.LogManager;
import com.dbms.transaction.LogRecord;
import com.dbms.transaction.Transaction;
import com.dbms.storage.BufferPoolManager;

import java.io.IOException;

/**
 * 【已修复】
 * DeleteExecutor (删除执行器) 负责处理元组的删除操作。
 * 这个版本的核心修复是重写了外键检查逻辑。现在，它不再仅仅依赖于元数据，
 * 而是会精确地扫描所有可能引用当前表的其他表，
 * 检查是否有任何实际存在的行正在通过外键引用即将被删除的键，从而保证了数据完整性。
 */
public class DeleteExecutor implements Executor {
    /**
     * 缓冲池管理器，用于从磁盘获取页面并管理内存中的页面。
     */
    private final BufferPoolManager bufferPoolManager;

    /**
     * 要删除元组的表的元数据信息。
     */
    private final Catalog.TableMetadata tableInfo;

    /**
     * 要删除的元组的主键值。
     */
    private final Integer keyToDelete;

    /**
     * 标志位，确保删除操作只执行一次。
     */
    private boolean hasExecuted = false;

    /**
     * 当前事务的上下文，用于日志记录。
     */
    private final Transaction transaction;

    /**
     * 日志管理器，用于写入删除操作的日志。
     */
    private final LogManager logManager;

    /**
     * 目录管理器，用于获取数据库中所有表的元数据，以进行外键检查。
     */
    private final Catalog catalog;

    /**
     * 完整的构造函数。
     * @param bpm 缓冲池管理器
     * @param tableInfo 表元数据
     * @param key 要删除的主键
     * @param transaction 事务
     * @param logManager 日志管理器
     * @param catalog 目录管理器
     */
    public DeleteExecutor(BufferPoolManager bpm, Catalog.TableMetadata tableInfo, Integer key, Transaction transaction, LogManager logManager, Catalog catalog) {
        this.bufferPoolManager = bpm;
        this.tableInfo = tableInfo;
        this.keyToDelete = key;
        this.transaction = transaction;
        this.logManager = logManager;
        this.catalog = catalog;
    }

    /**
     * 用于向后兼容的构造函数（没有事务和日志）。
     * @param bpm 缓冲池管理器
     * @param tableInfo 表元数据
     * @param key 要删除的主键
     */
    public DeleteExecutor(BufferPoolManager bpm, Catalog.TableMetadata tableInfo, Integer key) {
        this(bpm, tableInfo, key, null, null, null); // For backward compatibility
    }

    /**
     * 打开执行器，重置执行状态。
     */
    @Override
    public void open() { hasExecuted = false; }

    /**
     * 执行删除操作。
     * @return 总是返回 null，因为 DML 操作不产生输出元组。
     */
    @Override
    public Tuple next() {
        if (hasExecuted) return null;
        Page page = null;
        try {
            // 【核心修复】在执行任何删除操作之前，首先进行精确的外键引用检查。
            checkForReferencingTuples();

            // 1. 使用索引查找要删除的键对应的 RID (记录标识符)
            RID rid = tableInfo.index.find(keyToDelete);
            if (rid == null) {
                System.out.println("Key " + keyToDelete + " not found. Nothing to delete.");
                hasExecuted = true;
                return null;
            }
            // 2. 根据 RID 从缓冲池中获取包含该元组的页面
            page = bufferPoolManager.fetchPage(rid.getPageId());
            // 3. 对页面加写锁，因为我们要修改它
            page.wLock();
            try {
                TablePage tablePage = new TablePage(page);
                byte[] tupleData = tablePage.getTuple(rid.getSlotNum());
                if (tupleData == null) return null; // 元组已被删除

                // 4. 如果在事务中，则记录删除日志
                if (transaction != null && logManager != null) {
                    Tuple deletedTuple = new Tuple(tupleData, tableInfo.schema);
                    LogRecord logRecord = new LogRecord(transaction.getTransactionId(), tableInfo.tableName, deletedTuple);
                    logManager.appendLogRecord(logRecord);
                }
                // 5. 从页面中删除元组
                if (tablePage.deleteTuple(rid.getSlotNum())) {
                    // 6. 如果页面删除成功，则从索引中也删除该键
                    tableInfo.index.delete(keyToDelete);
                }
            } finally {
                // 7. 解锁并解钉页面。因为我们执行了删除，所以页面是脏的 (dirty=true)。
                page.wUnlock();
                bufferPoolManager.unpinPage(page.getPageId(), true);
                hasExecuted = true;
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return null;
    }

    /**
     * 【新增方法】扫描数据库中的所有其他表，检查是否有任何元组通过外键引用了 `keyToDelete`。
     * 如果找到任何引用，则抛出异常，阻止删除操作。
     */
    private void checkForReferencingTuples() {
        // 遍历目录中的所有表名
        for (String referencingTableName : catalog.getTableNames()) {
            if (referencingTableName.equalsIgnoreCase(tableInfo.tableName)) {
                continue; // 跳过自己，不检查对自身的引用
            }

            // 获取可能引用当前表的表的元数据
            Catalog.TableMetadata referencingTable = catalog.getTable(referencingTableName);
            // 遍历该表的所有外键约束
            for (Catalog.ForeignKeyInfo fk : referencingTable.foreignKeys) {
                // 检查这个外键是否引用了我们正在删除记录的表
                if (fk.referencedTableName.equalsIgnoreCase(tableInfo.tableName)) {
                    // 找到了一个引用关系。现在需要实际扫描这个表，看是否有行的外键值等于我们要删除的键。
                    int fkColumnIndex = findColumnIndex(referencingTable.schema, fk.sourceColumnName);

                    // 创建一个顺序扫描执行器来读取引用表中的每一行
                    Executor scan = new SeqScanExecutor(bufferPoolManager, catalog, referencingTableName);
                    scan.open();
                    Tuple tuple;
                    while ((tuple = scan.next()) != null) {
                        Object fkValue = tuple.getValue(fkColumnIndex);
                        // 检查外键列的值是否与我们要删除的键相等
                        if (fkValue != null && fkValue.equals(keyToDelete)) {
                            scan.close(); // 及时关闭扫描器
                            // 如果找到，说明存在引用，违反了外键约束，必须抛出异常
                            throw new RuntimeException("Cannot delete record: foreign key constraint violation. " +
                                    "Key '" + keyToDelete + "' is still referenced by table '" + referencingTableName + "'.");
                        }
                    }
                    scan.close(); // 正常结束扫描后关闭扫描器
                }
            }
        }
    }

    /**
     * 在给定的 Schema 中查找列名对应的索引。
     * @param schema 表的模式
     * @param columnName 要查找的列名
     * @return 列的索引，如果未找到则返回 -1。
     */
    private int findColumnIndex(Schema schema, String columnName) {
        for (int i = 0; i < schema.getColumnCount(); i++) {
            if (schema.getColumn(i).getColumnName().equalsIgnoreCase(columnName)) {
                return i;
            }
        }
        return -1; // 在一致的 schema 中不应该发生
    }

    @Override
    public void close() {}

    @Override
    public Schema getSchema() { return null; }

    @Override
    public PlanNode getPlanNode() {
        return null;
    }
}